import numpy as np
import h5py
import matplotlib.pyplot as plt
from testCases import *
from dnn_utils import sigmoid, sigmoid_backward, relu, relu_backward

plt.rcParams['figure.figsize'] = (5.0, 4.0) # 设置绘图的默认大小
plt.rcParams['font.sans-serif'] = 'SimHei'
plt.rcParams['image.interpolation'] = 'nearest'
plt.rcParams['image.cmap'] = 'gray'

np.random.seed(1) # seed后面的数字一样，每次取出的随机数就一样

# 三、初始化====================================================================================
# 3.1 两层神经网络(创建并初始化2层神经网络参数)====================================================

def initialize_parameters(n_x, n_h, n_y):
    """
    参数:
    n_x——输入层的大小
    n_h——隐藏层的大小
    n_y——输出层的大小

    返回:
    parameters——包含你的参数的python字典:
    W1——形状权矩阵(n_h, n_x)
    b1——形状偏置向量(n_h, 1)
    W2——形状权矩阵(n_y, n_h)
    b2——形状(n_y, 1)的偏差向量
    """
    np.random.seed(1)

    W1 = np.random.randn(n_h, n_x)*0.01
    # np.zeros():返回来一个给定形状和类型的用0填充的数组；
    b1 = np.zeros((n_h, 1))
    W2 = np.random.randn(n_y, n_h)*0.01
    b2 = np.zeros((n_y, 1))

    assert(W1.shape == (n_h, n_x))
    assert(b1.shape == (n_h, 1))
    assert(W2.shape == (n_y, n_x))
    assert(b2.shape == (n_y, 1))

    parameters = {"W1": W1, "b1":b1, "W2":W2, "b2":b2}

    return parameters

# 验证
parameters = initialize_parameters(2,2,1)
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

# 3.2实现L层神经网络的初始化==================================================

def initialize_parameters_deep(layer_dims):
    """
    参数:
    layer_dimms——python数组(列表)，包含网络中每一层的尺寸

    返回:
    参数——python字典包含你的参数"W1"， "b1"，…,“WL”,“bL”:
    Wl——形状权矩阵(layer_dims[l]， layer_dims[l-1])
    bl——形状偏差向量(layer_dims[l]， 1)
    """
    np.random.seed(3)
    parameters = {}
    L = len(layer_dims)

    for l in range(1, L):
        parameters['W' + str(l)] = np.random.randn(layer_dims[l], layer_dims[l-1])*0.01
        parameters['b' + str(l)] = np.zeros((layer_dims[l],1))

        assert(parameters['W' + str(l)].shape == (layer_dims[l], layer_dims[l-1]))
        assert(parameters['b' + str(l)].shape == (layer_dims[l], 1))

    return parameters

# 验证
parameters = initialize_parameters_deep([5,4,3])
print("W1 = " + str(parameters["W1"]))
print("b1 = " + str(parameters["b1"]))
print("W2 = " + str(parameters["W2"]))
print("b2 = " + str(parameters["b2"]))

# 四、正向传播模块=============================================================================================
# 4.1 线性正向================================================================================================

def linear_forward(A, W, b):

    """
    实现一层向前传播的线性部分。

    参数:
    A——来自前一层的激活(或输入数据):(前一层的大小，示例数)
    W——权重矩阵:形状的numpy数组(当前层大小，前一层大小)
    b——偏置向量，numpy形状数组(当前层大小，1)

    返回:
    Z——激活函数的输入，也称为预激活参数
    缓存——包含" a "， "W"和"b"的python字典;存储用于有效地计算向后传递
    """
    Z = np.dot(W,A) + b

    assert(Z.shape == (W.shape[0], A.shape[1]))
    cache = (A, W, b)

    return Z, cache

# 验证
A, W, b = linear_forward_test_case()

Z, linear_cache = linear_forward(A, W, b)
print("Z = " + str(Z))

# 4.2 正向线性激活===================================================================================

def linear_activation_forward(A_prev, W, b, activation):
    """
    实现线性>激活层的正向传播

    参数:
    A_prev——从上一层激活(或输入数据):(上一层的大小，示例数)
    W——权重矩阵:形状的numpy数组(当前层大小，前一层大小)
    b——偏置向量，numpy形状数组(当前层大小，1)
    激活——在这一层中使用的激活，存储为一个文本字符串:“sigmoid”或“relu”

    返回:
    A——激活函数的输出，也叫激活后值
    cache——包含"linear_cache"和"activation_cache"的python字典;
    存储用于有效地计算向后传递
    """
    if activation == "sigmoid":
        # 输入:“A_prev, W, b”。输出:“activation_cache”。
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = sigmoid(Z)

    elif activation == "relu":
        # 输入:“A_prev, W, b”。输出:“activation_cache”。
        Z, linear_cache = linear_forward(A_prev, W, b)
        A, activation_cache = relu(Z)

    assert(A.shape == (W.shape[0], A_prev.shape[1]))
    cache = (linear_cache, activation_cache)

    return A, cache

# 验证
A_prev, W, b = linear_activation_forward_test_case()

A, linear_activation_cache = linear_activation_forward(A_prev, W, b, activation = "sigmoid")
print("With sigmoid: A = " + str(A))

A, linear_activation_cache = linear_activation_forward(A_prev, W, b, activation = "relu")
print("With ReLU: A = " + str(A))

# 4.3 L层模型=============================================================================================
def L_model_forward(X, parameters):
    """
    实现[LINEAR->RELU]*(L-1)->LINEAR->SIGMOID计算的正向传播

    参数:
    X——数据，numpy形状数组(输入大小，示例数)
    参数——initialize_parameters_deep()的输出

    返回:
    AL——最后激活后的值
    cache——包含以下内容的缓存列表:
    每个linear_relu_forward()的缓存(有L-1个，索引从0到L-2)
    linear_sigmoid_forward()(有一个索引L-1)的缓存
    """
    caches = []
    A = X
    L = len(parameters) // 2

    # 实现[线性-> RELU]*(L-1)。将“cache”添加到“caches”列表中。
    for l in range(1, L):
        A_prev = A

        A, cache = linear_activation_forward(A_prev,parameters['W' + str(l)],parameters['b' + str(l)],activation = "relu")
        caches.append(cache)

    AL, cache = linear_activation_forward(A,parameters['W' + str(L)],parameters['b' + str(L)],activation = "sigmoid")
    caches.append(cache)

    assert(AL.shape == (1,X.shape[1]))

    return AL, caches

# 验证
X, parameters = L_model_forward_test_case()
AL, caches = L_model_forward(X, parameters)
print("AL = " + str(AL))
print("Length of caches list = " + str(len(caches)))

# 五、损失函数=====================================================================================
# 使用以下公式计算交叉熵损失J

def compute_cost(AL, Y):
    """
    实现式(7)定义的成本函数。

    参数:
    AL——对应于标签预测的概率向量，形状(1，样本数)
    Y——true“label”向量(例如:如果非猫则包含0，如果猫则包含1)，形状(1，样本数)

    返回:
    损失——交叉熵损失
    """
    m = Y.shape[1]

    cost = -1 / m * np.sum(Y * np.log(AL) + (1-Y) * np.log(1-AL),axis=1,keepdims=True)

    cost = np.squeeze(cost)     #   确保你的成本形状符合我们的预期(例如，将[[17]]变为17)
    assert(cost.shape == ())

    return cost

# 验证
Y, AL = compute_cost_test_case()

print("cost = " + str(compute_cost(AL, Y)))

# 六、反向传播模块==================================================================================
# 6.1 线性反向=====================================================================================
def linear_backward(dZ, cache):
    """
    为单层(层l)实现反向传播的线性部分

    参数:
    dZ——成本相对于(当前l层)线性输出的梯度
    缓存——来自当前层前向传播的值(A_prev, W, b)的元组

    返回:
    dA_prev——成本相对于激活(前一层l-1)的梯度，形状与A_prev相同
    dW——成本相对于W(当前l层)的梯度，形状与W相同
    db——成本相对于b(当前层l)的梯度，形状与b相同
    """
    A_prev, W, b = cache
    m = A_prev.shape[1]

    dW = 1 / m * np.dot(dZ, A_prev.T)
    db = 1 / m * np.sum(dZ, axis = 1, keepdims=True )
    dA_prev = np.dot(W.T, dZ)

    assert(dA_prev.shape == A_prev.shape)
    assert(dW.shape == W.shape)
    assert(db.shape == b.shape)

    return dA_prev, dW, db

# 验证
dZ, linear_cache = linear_backward_test_case()

dA_prev, dW, db = linear_backward(dZ, linear_cache)
print ("dA_prev = "+ str(dA_prev))
print ("dW = " + str(dW))
print ("db = " + str(db))

# 6.2 反向线性激活(实现LINEAR->ACTIVATION 层的反向传播。)====================================================

def linear_activation_backward(dA, cache, activation):
    """
    实现线性>激活层的反向传播。

    参数:
    dA——当前层l的激活后梯度
    缓存——我们存储的值元组(linear_cache, activation_cache)是为了有效地计算反向传播
    激活——在这一层中使用的激活，存储为一个文本字符串:“sigmoid”或“relu”

    返回:
    dA_prev——成本相对于激活(前一层l-1)的梯度，形状与A_prev相同
    dW——成本相对于W(当前l层)的梯度，形状与W相同
    db——成本相对于b(当前层l)的梯度，形状与b相同
    """
    linear_cache, activation_cache = cache

    if activation == "relu":
        dZ = relu_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)

    elif activation == "sigmoid":
        dZ = sigmoid_backward(dA, activation_cache)
        dA_prev, dW, db = linear_backward(dZ, linear_cache)

    return dA_prev, dW, db

# 验证
AL, linear_activation_cache = linear_activation_backward_test_case()

dA_prev, dW, db = linear_activation_backward(AL, linear_activation_cache, activation = "sigmoid")
print ("sigmoid:")
print ("dA_prev = "+ str(dA_prev))
print ("dW = " + str(dW))
print ("db = " + str(db) + "\n")

dA_prev, dW, db = linear_activation_backward(AL, linear_activation_cache, activation = "relu")
print ("relu:")
print ("dA_prev = "+ str(dA_prev))
print ("dW = " + str(dW))
print ("db = " + str(db))

# 6.3 反向L层模型(实现 [LINEAR->RELU]  (L-1) -> LINEAR -> SIGMOID 模型的反向传播。)============================================

def L_model_backward(AL, Y, caches):
    """
    为[LINEAR-> RELU] *（L-1）-> LINEAR-> SIGMOID组实现向后传播
    
    参数：
    AL-概率向量，前向传播的输出（L_model_forward（））
    Y-真正的“标签”向量（如果非cat，则包含0；如果cat，则包含1）
    caches-包含以下内容的缓存列表：
                每个带有“ relu”的linear_activation_forward（）cache它是caches[l]，对于范围（L-1）中的l，即l = 0 ... L-2）
                具有“ Sigmoid”的linear_activation_forward（）的cache（它是caches[L-1]）
    
    返回值：
    grads-带有渐变的字典
            grads [“ dA” + str（l）] = ...
            grads [“ dW” + str（l）] = ...
            grads [“ db” + str（l）] = ...
    """
    grads = {}
    L =len(caches)
    m = AL.shape[1]
    Y = Y.reshape(AL.shape)# after this line, Y is the same shape as AL

    # 初始化反向传播
    dAL = -(np.divide(Y, AL) - np.divide(1 - Y, 1 - AL))
    
    # 第L层（SIGMOID-> LINEAR）渐变。 输入：“ AL，Y，cache”。 输出：“ grads [” dAL“]，grads [” dWL“]，grads [” dbL“] 
    current_cache = caches[L - 1]
    grads["dA" + str(L)], grads["dW" + str(L)], grads["db" + str(L)] = linear_activation_backward(dAL, current_cache, activation = "sigmoid")

    for l in reversed(range(L - 1)):
        # lth层：（RELU-> LINEAR）渐变。
        # Inputs: "grads["dA" + str(l + 2)], caches". Outputs: "grads["dA" + str(l + 1)] , grads["dW" + str(l + 1)] , grads["db" + str(l + 1)] 
        current_cache = caches[l]
        dA_prev_temp, dW_temp, db_temp = linear_activation_backward(grads['dA' + str(l+2)], current_cache, activation = "relu")
        grads["dA" + str(l + 1)] = dA_prev_temp
        grads["dW" + str(l + 1)]= dW_temp
        grads["db" + str(l + 1)]= db_temp

    return grads

# 验证
AL, Y_assess, caches = L_model_backward_test_case()
grads = L_model_backward(AL, Y_assess, caches)
print ("dW1 = "+ str(grads["dW1"]))
print ("db1 = "+ str(grads["db1"]))
print ("dA1 = "+ str(grads["dA1"]))

# 更新参数(实现update_parameters（）以使用梯度下降来更新模型参数。)==================================================

def update_parameters(parameters, grads, learning_rate):
    """
    使用梯度下降更新参数

    参数:
    参数——包含参数的python字典
    gradient——包含梯度的python字典，L_model_backward的输出

    返回:
    参数——python字典，包含你更新的参数
    参数["W" + str(l)] =…
    参数["b" + str(l)] =…
    """
    L = len(parameters) // 2

    # 每个参数的更新规则。使用for循环。
    for l in range(L):
        parameters["W" + str(l + 1)] = parameters["W" + str(l + 1)] - learning_rate * grads["dW" + str(l + 1)]
        parameters["b" + str(l + 1)] = parameters["b" + str(l + 1)] - learning_rate * grads["db" + str(l + 1)]

    return parameters

# 验证
parameters, grads = update_parameters_test_case()
parameters = update_parameters(parameters, grads, 0.1)

print ("W1 = "+ str(parameters["W1"]))
print ("b1 = "+ str(parameters["b1"]))
print ("W2 = "+ str(parameters["W2"]))
print ("b2 = "+ str(parameters["b2"]))
